package org.budo.mybatis.criteria.query.parser.impl;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Id;

import org.budo.mybatis.criteria.query.CriteriaQuery;
import org.budo.mybatis.criteria.query.ParsedCriteriaQuery;
import org.budo.mybatis.criteria.query.parser.CriteriaQueryParser;
import org.budo.support.assertion.Assert;
import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.lang.util.StringUtil;

/**
 * @author li
 */
public class CriteriaQueryParserImpl implements CriteriaQueryParser {
    @Override
    public ParsedCriteriaQuery parseCriteriaQuery(CriteriaQuery criteriaQuery) {
        ParsedCriteriaQuery parsedCriteriaQuery = new ParsedCriteriaQuery(criteriaQuery);

        this.parseWhere(parsedCriteriaQuery);// where 解析

        this.parseSelect(parsedCriteriaQuery); // select 解析

        this.parseJoin(parsedCriteriaQuery); // join 解析，会参考where和select

        return parsedCriteriaQuery;
    }

    private void parseSelect(ParsedCriteriaQuery parsedCriteriaQuery) {
        parsedCriteriaQuery.getSelectSql().append(" SELECT 1 FROM user t ");
    }

    private void parseJoin(ParsedCriteriaQuery parsedCriteriaQuery) {
        for (TableJoinConfig tableJoinConfig : parsedCriteriaQuery.getTableJoinConfigs()) {
            parsedCriteriaQuery.getJoinSql()
                    .append(" LEFT JOIN " + tableJoinConfig.getJoinTable() //
                            + " " + tableJoinConfig.getJoinTableAlias() //
                            + " ON " + tableJoinConfig.getJoinTableAlias() + "." + tableJoinConfig.getJoinTableColumn() //
                            + "=" + tableJoinConfig.getJoinReferTable() + "." + tableJoinConfig.getJoinReferTableColumn());
        }
    }

    private void parseWhere(ParsedCriteriaQuery parsedCriteriaQuery) {
        this.parseWhere(parsedCriteriaQuery.getFilter(), parsedCriteriaQuery);
    }

    private void parseWhere(Map<String, Object> filter, ParsedCriteriaQuery parsedCriteriaQuery) {
        Set<Entry<String, Object>> filterEntrySet = filter.entrySet();
        for (Entry<String, Object> filterEntry : filterEntrySet) {
            String key = filterEntry.getKey();
            if ("$eq".equals(key)) {
                this.parseCompareValue(" = ", filterEntry, parsedCriteriaQuery);
            } else if ("$ne".equals(key)) {
                this.parseCompareValue(" != ", filterEntry, parsedCriteriaQuery);
            } else if ("$gt".equals(key)) {
                this.parseCompareValue(" > ", filterEntry, parsedCriteriaQuery);
            } else if ("$ge".equals(key)) {
                this.parseCompareValue(" >= ", filterEntry, parsedCriteriaQuery);
            } else if ("$lt".equals(key)) {
                this.parseCompareValue(" < ", filterEntry, parsedCriteriaQuery);
            } else if ("$le".equals(key)) {
                this.parseCompareValue(" <= ", filterEntry, parsedCriteriaQuery);
            } else if ("$and".equals(key)) {
                this.parseAssemble(" AND ", filterEntry, parsedCriteriaQuery);
            } else if ("$or".equals(key)) {
                this.parseAssemble(" OR ", filterEntry, parsedCriteriaQuery);
            } else if ("$in".equals(key)) {
                this.parseCompareArray(" IN ", filterEntry, parsedCriteriaQuery);
            } else if ("$notin".equals(key)) {
                this.parseCompareArray(" NOT IN ", filterEntry, parsedCriteriaQuery);
            } else {
                throw new RuntimeException("key=" + key);
            }
        }
    }

    private void parseAssemble(String operator, Entry<String, Object> filterEntry, ParsedCriteriaQuery parsedCriteriaQuery) {
        Object value = filterEntry.getValue();
        List<Map<String, Object>> assembleList = (List<Map<String, Object>>) value;

        int size = assembleList.size();
        for (int i = 0; i < size; i++) {
            parsedCriteriaQuery.getWhereSql().append("( ");
            Map<String, Object> andItemFilter = assembleList.get(i);
            this.parseWhere(andItemFilter, parsedCriteriaQuery); // 递归
            parsedCriteriaQuery.getWhereSql().append(" ) ");
            if (i < size - 1) {
                parsedCriteriaQuery.getWhereSql().append(operator);
            }
        }
    }

    private void parseCompareArray(String operator, Entry<String, Object> filterEntry, ParsedCriteriaQuery parsedCriteriaQuery) {
        Object value = filterEntry.getValue();
        Map<String, Object> valueMap = (Map<String, Object>) value;
        Entry<String, Object> valueEntry = valueMap.entrySet().iterator().next();

        String field = valueEntry.getKey();
        Object fieldValue = valueEntry.getValue();

        String column = this.whereFieldToColumn(field, parsedCriteriaQuery);
        Object rightValue = this.rightValue(fieldValue);
        parsedCriteriaQuery.getWhereSql().append(column).append(operator).append(" ( ").append(rightValue).append(" ) ");
    }

    private void parseCompareValue(String operator, Entry<String, Object> filterEntry, ParsedCriteriaQuery parsedCriteriaQuery) {
        Object value = filterEntry.getValue();
        Map<String, Object> valueMap = (Map<String, Object>) value;
        Entry<String, Object> valueEntry = valueMap.entrySet().iterator().next();

        String field = valueEntry.getKey();
        Object fieldValue = valueEntry.getValue();

        String column = this.whereFieldToColumn(field, parsedCriteriaQuery);
        Object rightValue = this.rightValue(fieldValue);
        parsedCriteriaQuery.getWhereSql().append(column).append(operator).append(rightValue);
    }

    /**
     * 右值，占位符 或 具体值
     */
    private Object rightValue(Object fieldValue) {
        if (null != fieldValue && fieldValue instanceof String) {
            String stringValue = (String) fieldValue;
            if (stringValue.startsWith("$")) {
                return "#{" + stringValue.substring(1) + "}";
            }
        }

        if (fieldValue instanceof List) {
            List<Object> list = (List<Object>) fieldValue;
            return StringUtil.join(list, ",", "'", "'");
        }

        return "'" + fieldValue + "'";
    }

    /**
     * 这里是在where里面，别名不管
     */
    private String whereFieldToColumn(String fieldName, ParsedCriteriaQuery parsedCriteriaQuery) {
        if (fieldName.contains(".")) {
            return this.whereFieldToColumnWithJoin(fieldName, parsedCriteriaQuery);
        }
        return this.whereFieldToColumnNoJoin(fieldName, parsedCriteriaQuery);
    }

    private String whereFieldToColumnNoJoin(String fieldName, ParsedCriteriaQuery parsedCriteriaQuery) {
        Field fieldEntity = ReflectUtil.getField(parsedCriteriaQuery.getEntityType(), fieldName);
        Assert.notNull(fieldEntity, "field not found, type=" + parsedCriteriaQuery.getEntityType() + ", fieldName=" + fieldName);

        Id id = fieldEntity.getAnnotation(Id.class);

        if (null != id) {
            return "t." + fieldEntity.getName();
        }

        Column column = fieldEntity.getAnnotation(Column.class);
        if (null != column) {
            if (StringUtil.isEmpty(column.name())) {
                return "t." + fieldEntity.getName();
            } else {
                return "t." + column.name();
            }
        }

        throw new RuntimeException("fieldName=" + fieldName + ", type=" + parsedCriteriaQuery.getEntityType() + ", has no @Id or @Column");
    }

    private String whereFieldToColumnWithJoin(String fieldName, ParsedCriteriaQuery parsedCriteriaQuery) {
        Class<?> entityType = parsedCriteriaQuery.getEntityType();
        String _field = new String(fieldName);
        while (true) {
            int index = _field.indexOf('.');

            String joinFieldName = _field.substring(0, index);
            Field field = ReflectUtil.getField(entityType, joinFieldName);

            TableJoinConfig tableJoinConfig = new TableJoinConfig();
            tableJoinConfig.setJoinTable(field.getName());
            parsedCriteriaQuery.addTableJoinConfig(tableJoinConfig);

            if (index < 0) {
                break;
            } else {
                _field = _field.substring(index + 1);
            }
        }

        return null;
    }
}